Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an SSVI (Surface SVI) volatility parametrisation to the options module, along with documentation and site enhancements to support bibliographic cross-references and richer MkDocs output.
Changes:
- Add
SSVImodel with calibration helpers (fit,fit_surface) and comprehensive unit tests. - Improve docs bibliography linking and styling (BibTeX entries + MkDocs config updates), and add an API docs page for SSVI.
- Miscellaneous robustness/typing tweaks across app APIs and options pricing.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| quantflow/ta/paths.py | Adds typing annotation for tau list in Hurst exponent calculation. |
| quantflow/options/svi.py | Updates SVI docstring to link to bibliography entries. |
| quantflow/options/surface.py | Forces Black price sum to float before sigfig/Decimal conversion. |
| quantflow/options/ssvi.py | Introduces new SSVI model, analytics, and calibration routines. |
| quantflow_tests/test_ssvi.py | Adds tests covering SSVI shape, variance/IV, arbitrage checks, and calibration. |
| pyproject.toml | Bumps pandas + dev tooling minimum versions. |
| mkdocs.yml | Adds site description, nav entry for SSVI, enables md_in_html, and includes extra CSS. |
| docs/stylesheets/bibliography.css | Adds bibliography entry highlighting/styling. |
| docs/references.bib | Adds Gatheral SVI and Gatheral-Jacquier references. |
| docs/contributing.md | Tightens wording around responsibility for AI-assisted contributions. |
| docs/bibliography.md | Wrapes bibliography entries for styling and fragment targeting. |
| docs/bib2md.py | Changes bibliography generator output to emit wrapped entries. |
| docs/api/options/ssvi.md | Adds mkdocstrings page for quantflow.options.ssvi.SSVI. |
| app/utils/paths.py | Injects social OpenGraph/Twitter meta tags into MkDocs pages. |
| app/api/volatility.py | Ensures ttm_grid is JSON-friendly (float list). |
| app/api/cointegration.py | Suppresses ComplexWarning and coerces eigenvector to real values. |
| .github/instructions/release.instructions.md | Adds applyTo front matter. |
| .github/instructions/makefile.instructions.md | Adds applyTo front matter. |
| .github/copilot-instructions.md | Fixes documented test command and doc examples output path. |
Comments suppressed due to low confidence (2)
quantflow/options/ssvi.py:160
fit()usesnp.interp(0.0, k_arr, w_obs)and then runs least-squares without checking that inputs are non-empty, same-shape, and sorted byk.np.interprequires an increasing x-grid, and empty/mismatched inputs will currently producenaninitial guesses or broadcast errors.
k_arr = np.asarray(k, dtype=float)
iv_arr = np.asarray(iv, dtype=float)
w_obs = iv_arr**2 * ttm
atm_var = float(np.interp(0.0, k_arr, w_obs)) if k_arr.size else w_obs.mean()
x0 = [0.0, 1.0, max(atm_var, 1e-4)]
quantflow/options/ssvi.py:213
fit_surface()collects slices without validating/sorting each(k, iv)pair. This can break the ATM interpolation (np.interpexpects sortedk) and can yieldnaninitial thetas if a slice is empty.
data = []
thetas0 = []
for k, iv, ttm in slices:
k_arr = np.asarray(k, dtype=float)
iv_arr = np.asarray(iv, dtype=float)
w_obs = iv_arr**2 * ttm
data.append((k_arr, w_obs))
atm = float(np.interp(0.0, k_arr, w_obs)) if k_arr.size else w_obs.mean()
thetas0.append(max(atm, 1e-4))
| def iv( | ||
| self, | ||
| k: Annotated[ArrayLike, Doc("Log-moneyness log(K/F), scalar or array")], | ||
| ttm: Annotated[float, Doc("Time to maturity in years")], | ||
| ) -> np.ndarray: | ||
| r"""Implied volatility $\sigma(k) = \sqrt{w(k) / \tau}$. | ||
|
|
||
| Returns an array of the same shape as $k$. The SSVI total variance is | ||
| strictly positive for $|\rho| < 1$, so no clipping is required. | ||
| """ | ||
| return np.sqrt(self.total_variance(k) / ttm) |
| if suffix: | ||
| body = f"{body}, {suffix}" | ||
|
|
||
| return f"#### {key}\n\n{body}\n" | ||
| return f'<div class="bib-entry" markdown>\n\n#### {key}\n\n{body}\n\n</div>\n' | ||
|
|
|
|
||
| --- | ||
|
|
||
| <div class="bib-entry" markdown> |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #86 +/- ##
==========================================
+ Coverage 88.75% 88.87% +0.11%
==========================================
Files 84 85 +1
Lines 5142 5302 +160
==========================================
+ Hits 4564 4712 +148
- Misses 578 590 +12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
quantflow/options/ssvi.py:328
fit_surface()doesn’t validate that each slice has non-emptyk/ivarrays of matching shape. Empty slices will yieldnaninitial thetas and mismatched shapes can broadcast, producing incorrect residual vectors.
for k, iv, ttm in slices:
k_arr = np.asarray(k, dtype=float)
iv_arr = np.asarray(iv, dtype=float)
w_obs = iv_arr**2 * ttm
data.append((k_arr, w_obs, float(ttm)))
| k_arr = np.asarray(k, dtype=float) | ||
| iv_arr = np.asarray(iv, dtype=float) | ||
| w_obs = iv_arr**2 * ttm |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
quantflow/options/ssvi.py:276
fit_surface()usesttmdirectly without validating it is strictly positive, and usesnp.interp(…, k_arr, …)without sortingk_arr. Ifttm <= 0this will divide by zero in the residual normalisation, and ifk_arris not monotone increasingnp.interpcan produce incorrect ATM variance seeds.
for k, iv, ttm in slices:
k_arr = np.asarray(k, dtype=float)
iv_arr = np.asarray(iv, dtype=float)
if k_arr.size == 0 or iv_arr.size == 0:
raise ValueError("k and iv must contain at least one quote")
if k_arr.shape != iv_arr.shape:
quantflow/options/ssvi.py:262
- This docstring uses a fully-qualified mkdocstrings cross-reference to
VarianceCurve, but the target is in the same module. The repo instructions prefer relative cross-references ([...][.VarianceCurve]) when the target is in scope.
monotone [VarianceCurve][quantflow.options.ssvi.VarianceCurve]. The
quantflow/options/ssvi.py:352
fit_vol_surface()can returncls.fit_surface([])when there are no converged options across all maturities, which raises a generic "at least one maturity slice" error. Adding an explicit check here yields a clearer error for callers (including the API endpoint).
slices = []
for index, maturity in enumerate(surface.maturities):
options = list(surface.option_prices(index=index, converged=True))
if not options:
continue
| r"""Implied volatility $\sigma(k, \tau) = \sqrt{w(k, \tau) / \tau}$. | ||
|
|
||
| Returns an array of the same shape as $k$. The SSVI total variance is | ||
| strictly positive for $|\rho| < 1$, so no clipping is required. | ||
| """ |
| with warnings.catch_warnings(): | ||
| # statsmodels assigns complex eigenvalue statistics into real arrays, | ||
| # discarding noise-level imaginary parts | ||
| warnings.simplefilter("ignore", np.exceptions.ComplexWarning) |
| max_ttm = max(float(op.ttm) for op in options) if options else 1.0 | ||
| ttm_grid = list(np.linspace(1 / 365, max_ttm, 50)) | ||
| ttm_grid = [float(t) for t in np.linspace(1 / 365, max_ttm, 50)] | ||
|
|
||
| return VolSurfaceResponse( | ||
| ssvi=SSVI.fit_vol_surface(surface), | ||
| inputs=inputs, | ||
| options=options, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 21 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
quantflow/options/ssvi.py:209
- The
iv()docstring says the result has the same shape ask, but the implementation returns the NumPy-broadcasted shape ofkandttm(e.g. scalarkwith vectorttmreturns a vector). This is misleading for API consumers.
r"""Implied volatility $\sigma(k, \tau) = \sqrt{w(k, \tau) / \tau}$.
Returns an array of the same shape as $k$. The SSVI total variance is
strictly positive for $|\rho| < 1$, so no clipping is required.
"""
| for k, iv, ttm in slices: | ||
| k_arr = np.asarray(k, dtype=float) | ||
| iv_arr = np.asarray(iv, dtype=float) | ||
| if k_arr.size == 0 or iv_arr.size == 0: | ||
| raise ValueError("k and iv must contain at least one quote") | ||
| if k_arr.shape != iv_arr.shape: | ||
| raise ValueError("k and iv must have the same shape") | ||
| w_obs = iv_arr**2 * ttm | ||
| data.append((k_arr, w_obs, float(ttm))) | ||
| atm = float(np.interp(0.0, k_arr, w_obs)) | ||
| thetas0.append(max(atm, 1e-4)) |
No description provided.